package client;

import server.HTTPServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;

/**
 * Classe per la definizione di un client generico sul server, normalmente identificato come "utente".
 * Viene gestito come se fosse un thread: una volta eseguito il thread, questo rimane in attesa di un comando,
 * quando arriva un comando viene subito elaborato e viene fornita la risposta. Normalmente l'azione di default
 * è l'eco dei messaggi ricevuti e l'inoltro agli altri client connessi.
 * @author Claudio "Dna" Bonesana
 */
public class GenericClientOnServer implements Runnable{

	private Socket socket;
	private int id;
	private GregorianCalendar gc = new GregorianCalendar();
	private String name;
	private HTTPServer parent;

	private BufferedReader in;
	private PrintWriter out;

    private long lastUpdate;
    private boolean working;
	
	private Map<String, CommandAction> commands = new HashMap<String, CommandAction>();

    /**
     * Il costruttore di questa classe si preoccupa di popolare la mappa dei comandi disponibili.
     * Alla creazione dell'oggetto viene notificato al log del server l'aggiunta di un nuovo utente.
     * @param socket    socket assegnato dal server
     * @param id        identificativo del client attuale, equivale all'id del thread
     * @param parent    riferimento al server
     */
	public GenericClientOnServer(Socket socket, int id, HTTPServer parent) {
		this.socket = socket;
		this.id = id;
		this.parent = parent;
		this.name = ""+id;

        this.lastUpdate = System.currentTimeMillis();
        this.working = true;

		/*  show current command list   */
        commands.put("/help", new CommandAction() {
            @Override
            public void performAction(GenericClientOnServer gcos, String txt) {
                System.out.println("[" + gcos.id + "] " + gcos.socket + " REQUEST: HELP");
                for (String key : gcos.commands.keySet()){
                    gcos.parent.update(gcos.id, "" + key);
                }
            }
        });
		/* 	show current server time 	*/
		commands.put("/time", new CommandAction() {	
			@Override
			public void performAction(GenericClientOnServer gcos, String txt) {
				System.out.println("[" + gcos.id + "] " + gcos.socket + " REQUEST: TIME");
                gcos.parent.update(gcos.id, getTime());
			}
		});
		/*	show current server date 	*/
		commands.put("/date", new CommandAction() {	
			@Override
			public void performAction(GenericClientOnServer gcos, String txt) {
				System.out.println("[" + gcos.id + "] " + gcos.socket + " REQUEST: DATE");
                gcos.parent.update(gcos.id, "<span style=\"color: red\">" + getDate() + "</span>");
			}
		});
		/* 	reverse a text message 	*/
		commands.put("/rev", new CommandAction() {	
			@Override
			public void performAction(GenericClientOnServer gcos, String txt) {
				System.out.println("[" + gcos.id + "] " + gcos.socket + " MSG: " + txt);
				StringBuffer reverse = new StringBuffer(txt).reverse();
                gcos.parent.update(gcos.id, reverse.toString());
			}
		});
		/*	show online users with ids 	*/
		commands.put("/users", new CommandAction() {
			@Override
			public void performAction(GenericClientOnServer gcos, String txt) {
				System.out.println("[" + gcos.id + "] " + gcos.socket + " REQUEST: USER LIST");
				for(GenericClientOnServer user : gcos.parent.getUsers()){
                    gcos.parent.update(gcos.id, "[" + user.getId() + "] " + user.getName());
				}
			}
		});
		/*	/whisp:user: message bla bla bla	*/
		commands.put("/whisp", new CommandAction() {
			@Override
			public void performAction(GenericClientOnServer gcos, String txt) {
				String[] arr = txt.split(":");
				System.out.println("[" + gcos.id + "] " + gcos.socket + " WHISPTO: user " + arr[1]);
				int i = Integer.parseInt(arr[1]);
                gcos.parent.update(gcos.id,"<span style=\"font-weight: bold\">" + name + "</span> to <span style=\"font-style: italic\">[" + gcos.id + "] " + gcos.name + ":</span> : " + arr[2]);
				gcos.parent.update(i, "<span style=\"font-style: italic\">[" + gcos.id + "] " + gcos.name + ":</span> " + arr[2]);
            }
		});
		/* 	disconnect client	*/
		commands.put("/exit", new CommandAction() {
			@Override
			public void performAction(GenericClientOnServer gcos, String txt) {
                System.out.print("[" + gcos.id + "] " + gcos.socket + " KICKED:  user ");
                gcos.parent.update(gcos.id, "/kick");
			}
		});
		/* 	ping command	*/
		commands.put("/ping", new CommandAction() {
			@Override
			public void performAction(GenericClientOnServer gcos, String txt) {
				gcos.parent.update(gcos.id, "/pong");
			}
		});
        /*  add icon command */
        commands.put("/addicon", new CommandAction() {
            @Override
            public void performAction(GenericClientOnServer gcos, String txt) {
                String[] arr = txt.split(" ");
                gcos.parent.addSmiles(arr[1],arr[2]);
                System.out.println("[" + gcos.id + "] " + gcos.socket + " ADDICON: " + arr[1] + " -> " + arr[2]);
                gcos.parent.update(gcos.name + " added a new icon: " + arr[1]);
            }
        });
        /*  whoami command  */
        commands.put("/whoami", new CommandAction() {
            @Override
            public void performAction(GenericClientOnServer gcos, String txt) {
                System.out.println("[" + gcos.id + "] " + gcos.socket + " WHOAMI?");
                gcos.parent.update(gcos.id, "You are [" + gcos.id + "] " + gcos.name);
            }
        });
		
		System.out.println("[" + id + "] " + socket + " CREATED");
	}

    /**
     * @return l'id attuale dell'oggetto
     */
	public int getId() {
		return id;
	}

    /**
     * @return il nome con cui l'utente ha deciso di identificarsi
     */
	public String getName() {
		return name;
	}

    /**
     * @return orario in unix timestamp dell'ultima comunicazione avvenuta
     */
    public long getLastUpdate() {
        return lastUpdate;
    }

    /**
     * Termina in sicurezza il thread
     */
    public synchronized void stopThread(){
        this.working = false;
    }

    /**
     * @return id e socket assegnati
     */
    @Override
	public String toString() {
		return "[" + id + "] " + socket;
	}

    /**
     * Esegue il thread. L'attesa avviene sulla ricezione di nuovi messaggi. All'arrivo, viene controllato se si tratta
     * di un comando da eseguire o se un messaggio da propagare. Prima di essere visualizzati su qualsiasi client i
     * messaggi passano tutti dal server che li propaga.
     */
	@Override
	public void run() {
		try {
			System.out.println(this + " STARTED");

            /*  Assegnamento dei canali di scrittura    */
			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));

			sendWelcomeMessage();                           /*  Invio del messaggio di benvenuto        */

            /*  Esecuzione della routine    */
			while (working) {
				String s = in.readLine();                   /*  Attesa bloccante di un nuovo mesaggio   */
                lastUpdate = System.currentTimeMillis();    /*  Aggiornamento dell'ultima azione        */
				if ( s == null ){
					break;                                  /*  Il socket è stato chiuso, quindi esce   */
				} else {
					boolean flag = true;
					for (String key : commands.keySet()) {  /*  Controllo per l'esecuzione dei comandi  */
						if (s.startsWith(key)){
							commands.get(key).performAction(this, s);
							out.flush();
							flag = false;
						}
					}
					if (flag){                              /*  Echo dei messaggi                       */
                        /*  Sistema gli smiles  */
                        for(String key : parent.getSmiles().keySet()){
                            s = s.replace(key,"<img src=\"" + parent.getSmiles().get(key) + "\"/>");
                        }
                        System.out.println("[" + id + "] " + socket + " MSG: " + s);
						parent.update("<span style=\"font-weight: bold\">" + name + "</span>: " + s);
					}
				}
			}
            /*  Chiusura del socket...  */
			in.close();
			out.close();
			socket.close();
			System.out.println(this + " STOPPED");
		} catch (IOException e) {
			System.err.println("Exception in thread " + id);
			e.printStackTrace();
        }
        /*  ...ed eliminazione del client */
        parent.removeClient(this,"User " + name + " <span style=\"color: red\">disconnected</span>!");
    }

    /**
     * Invia un messaggio al client collegato attraverso l'apposito canale.
     * @param msg   Messaggio da inviare al client
     */
	public void update(String msg){
		out.println(msg);
		out.flush();
	}

    /**
     * Invia un messaggio di benvenuto. Nel caso in cui ci sia una riconnessione, l'identificazione avviene con uno "/"
     * iniziale, quindi il messaggio di benvenuto cambia.
     * @throws IOException  nel caso in cui non si possa inviare un messaggio al client
     */
	private void sendWelcomeMessage() throws IOException{
        name = in.readLine();

        if (name.startsWith("/")){
            /*  Riconnessione   */
            name = name.replace("/","");
            update("Server <span style=\"color: green\">online</span>!");
        } else {
            /*  Nuovo utente    */
            update("My name is " + parent);
            update("Welcome " + name + " to this server!");
        }

		parent.update(name + " <span style=\"color: green\">connected</span>!", id);
	}

    /**
     * @return  l'attuale orario del server
     */
	private String getTime(){
		return gc.get(Calendar.HOUR) + ":" + gc.get(Calendar.MINUTE) + ":" + gc.get(Calendar.SECOND) + "." + gc.get(Calendar.MILLISECOND);
	}

    /**
     * @return  l'attuale data del server
     */
	private String getDate(){
		return gc.get(Calendar.YEAR) + "-" + gc.get(Calendar.MONTH) + "-" + gc.get(Calendar.DAY_OF_MONTH);
	}

}